package drds.data_propagate.driver.utils;

import java.io.ByteArrayOutputStream;
import java.io.IOException;

public abstract class ByteHelper {

    public static final long NULL_LENGTH = -1;

    public static byte[] readNullTerminatedBytes(byte[] bytes, int index) {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        for (int i = index; i < bytes.length; i++) {
            byte aByte = bytes[i];
            if (aByte == MSC.NULL_TERMINATED_STRING_DELIMITER) {
                break;
            }
            byteArrayOutputStream.write(aByte);
        }
        return byteArrayOutputStream.toByteArray();
    }

    public static void writeNullTerminatedString(String string, ByteArrayOutputStream byteArrayOutputStream) throws IOException {
        byteArrayOutputStream.write(string.getBytes());
        byteArrayOutputStream.write(MSC.NULL_TERMINATED_STRING_DELIMITER);
    }

    public static void writeNullTerminated(byte[] bytes, ByteArrayOutputStream byteArrayOutputStream) throws IOException {
        byteArrayOutputStream.write(bytes);
        byteArrayOutputStream.write(MSC.NULL_TERMINATED_STRING_DELIMITER);
    }

    public static byte[] readFixedLengthBytes(byte[] bytes, int index, int length) {
        byte[] newBytes = new byte[length];
        System.arraycopy(bytes, index, newBytes, 0, length);
        return newBytes;
    }

    /**
     * Read 4 bytes in Little-endian byte order.
     *
     * @param bytes, the original byte array
     * @param index, start to read from.
     * @return
     */
    public static long readUnsignedIntLittleEndian(byte[] bytes, int index) {
        long result = (long) (bytes[index] & 0xFF)//
                | (long) ((bytes[index + 1] & 0xFF) << 8)//
                | (long) ((bytes[index + 2] & 0xFF) << 16)//
                | (long) ((bytes[index + 3] & 0xFF) << 24);//
        return result;
    }

    public static long readUnsignedLongLittleEndian(byte[] bytes, int index) {
        long accumulation = 0;
        int position = index;
        for (int shiftBy = 0; shiftBy < 64; shiftBy += 8) {
            accumulation |= (long) ((bytes[position++] & 0xff) << shiftBy);
        }
        return accumulation;
    }

    public static int readUnsignedShortLittleEndian(byte[] bytes, int index) {
        int result = (bytes[index] & 0xFF) | ((bytes[index + 1] & 0xFF) << 8);
        return result;
    }

    public static int readUnsignedMediumLittleEndian(byte[] bytes, int index) {
        int result = (bytes[index] & 0xFF) | ((bytes[index + 1] & 0xFF) << 8) | ((bytes[index + 2] & 0xFF) << 16);
        return result;
    }

    public static long readLengthCodedBinary(byte[] bytes, int index) throws IOException {
        int firstByte = bytes[index] & 0xFF;
        switch (firstByte) {
            case 251:
                return NULL_LENGTH;
            case 252:
                return readUnsignedShortLittleEndian(bytes, index + 1);
            case 253:
                return readUnsignedMediumLittleEndian(bytes, index + 1);
            case 254:
                return readUnsignedLongLittleEndian(bytes, index + 1);
            default:
                return firstByte;
        }
    }

    public static byte[] readBinaryCodedLengthBytes(byte[] bytes, int index) throws IOException {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        byteArrayOutputStream.write(bytes[index]);

        byte[] newBytes = null;
        int value = bytes[index] & 0xFF;
        if (value == 251) {
            newBytes = new byte[0];
        }
        if (value == 252) {
            newBytes = new byte[2];
        }
        if (value == 253) {
            newBytes = new byte[3];
        }
        if (value == 254) {
            newBytes = new byte[8];
        }
        if (newBytes != null) {
            System.arraycopy(bytes, index + 1, newBytes, 0, newBytes.length);
            byteArrayOutputStream.write(newBytes);
        }

        return byteArrayOutputStream.toByteArray();
    }

    public static void write8ByteUnsignedIntLittleEndian(long longValue, ByteArrayOutputStream byteArrayOutputStream) {
        byteArrayOutputStream.write((byte) (longValue & 0xFF));
        byteArrayOutputStream.write((byte) (longValue >>> 8));
        byteArrayOutputStream.write((byte) (longValue >>> 16));
        byteArrayOutputStream.write((byte) (longValue >>> 24));
        byteArrayOutputStream.write((byte) (longValue >>> 32));
        byteArrayOutputStream.write((byte) (longValue >>> 40));
        byteArrayOutputStream.write((byte) (longValue >>> 48));
        byteArrayOutputStream.write((byte) (longValue >>> 56));
    }

    public static void writeUnsignedIntLittleEndian(long longValue, ByteArrayOutputStream byteArrayOutputStream) {
        byteArrayOutputStream.write((byte) (longValue & 0xFF));
        byteArrayOutputStream.write((byte) (longValue >>> 8));
        byteArrayOutputStream.write((byte) (longValue >>> 16));
        byteArrayOutputStream.write((byte) (longValue >>> 24));
    }

    public static void writeUnsignedInt64LittleEndian(long longValue, ByteArrayOutputStream byteArrayOutputStream) {
        byteArrayOutputStream.write((byte) (longValue & 0xFF));
        byteArrayOutputStream.write((byte) (longValue >>> 8));
        byteArrayOutputStream.write((byte) (longValue >>> 16));
        byteArrayOutputStream.write((byte) (longValue >>> 24));
        byteArrayOutputStream.write((byte) (longValue >>> 32));
        byteArrayOutputStream.write((byte) (longValue >>> 40));
        byteArrayOutputStream.write((byte) (longValue >>> 48));
        byteArrayOutputStream.write((byte) (longValue >>> 56));
    }

    public static void writeUnsignedShortLittleEndian(int intValue, ByteArrayOutputStream byteArrayOutputStream) {
        byteArrayOutputStream.write((byte) (intValue & 0xFF));
        byteArrayOutputStream.write((byte) ((intValue >>> 8) & 0xFF));
    }

    public static void writeUnsignedMediumLittleEndian(int intValue, ByteArrayOutputStream byteArrayOutputStream) {
        byteArrayOutputStream.write((byte) (intValue & 0xFF));
        byteArrayOutputStream.write((byte) ((intValue >>> 8) & 0xFF));
        byteArrayOutputStream.write((byte) ((intValue >>> 16) & 0xFF));
    }

    public static void writeBinaryCodedLengthBytes(byte[] bytes, ByteArrayOutputStream byteArrayOutputStream) throws IOException {
        // 1. write length byte/bytes
        if (bytes.length < 252) {
            byteArrayOutputStream.write((byte) bytes.length);
        } else if (bytes.length < (1 << 16L)) {
            byteArrayOutputStream.write((byte) 252);
            writeUnsignedShortLittleEndian(bytes.length, byteArrayOutputStream);
        } else if (bytes.length < (1 << 24L)) {
            byteArrayOutputStream.write((byte) 253);
            writeUnsignedMediumLittleEndian(bytes.length, byteArrayOutputStream);
        } else {
            byteArrayOutputStream.write((byte) 254);
            writeUnsignedIntLittleEndian(bytes.length, byteArrayOutputStream);
        }
        // 2. write real data followed length byte/bytes
        byteArrayOutputStream.write(bytes);
    }

    public static void writeFixedLengthBytes(byte[] bytes, int index, int length, ByteArrayOutputStream byteArrayOutputStream) {
        for (int i = index; i < index + length; i++) {
            byteArrayOutputStream.write(bytes[i]);
        }
    }

    public static void writeFixedLengthBytesFromStart(byte[] bytes, int length, ByteArrayOutputStream byteArrayOutputStream) {
        writeFixedLengthBytes(bytes, 0, length, byteArrayOutputStream);
    }

}
